using System;
using System.Collections.Generic;
using Picnic.Spec.Exceptions;

namespace Picnic.Spec
{
	/// <summary>
	/// Represents a specification.
	/// </summary>
	public abstract class Specification
	{
		private static readonly object _syncLock = new object();
		private readonly Stack<SpecificationDescriptionStage> _stages = new Stack<SpecificationDescriptionStage>();
		private List<StatementScope> _scopes;
		private List<Statement> _statements;
		private Action _beforeAction;

		public IEnumerable<StatementScope> GetStatementScopes()
		{
			lock (_syncLock)
			{
				if (_scopes == null)
				{
					SetStage(SpecificationDescriptionStage.OutOfScope);
					_scopes = new List<StatementScope>();
					SpecificationBody();
				}
				return _scopes.AsReadOnly();
			}
		}

		/// <summary>
		/// Describes this specification.
		/// </summary>
		protected abstract void SpecificationBody();

		/// <summary>
		/// Describes class <typeparam name="T" />.
		/// </summary>
		protected void Describe<T>(Action statements)
		{
			var type = typeof(T);
			Describe(type.Name, string.Empty, statements);
		}

		/// <summary>
		/// Describes class <typeparam name="T" /> in specified context.
		/// </summary>
		protected void Describe<T>(string context, Action statements)
		{
			var type = typeof(T);
			Describe(type.Name, context, statements);
		}

		/// <summary>
		/// Describes specified subject in specified context.
		/// </summary>
		protected void Describe(string subject, string context, Action body)
		{
			EnsureThatScopeIsNotNestedScope(subject, context);

			SetStage(SpecificationDescriptionStage.Scope);
			_statements = new List<Statement>();
			try
			{
				body();

				var scope = new StatementScope(subject, context, _statements);
				_scopes.Add(scope);
			}
			catch(SpecificationException)
			{
				throw;
			}
			catch(Exception error)
			{
				var statement = new Statement("should not raise exceptions", () => { throw error; });
				var scopeSubject = string.IsNullOrWhiteSpace(context)
					? "Statemnts scope \"" + subject + "\""
					: "Statemnts scope \"" + subject + " " + context + "\"";
				var scope = new StatementScope(scopeSubject, "initialization", new [] { statement });
				_scopes.Add(scope);
			}
			finally
			{
				_statements.Clear();
				_statements = null;
				_beforeAction = null;
				RedoStage();
			}
		}

		protected void Before(Action action)
		{
			EnsureThatBeforeActionDeclaredInContext();
			EnsureThatBeforeActionDeclaredOnce();

			SetStage(SpecificationDescriptionStage.BeforeAction);
			_beforeAction = action;
			RedoStage();
		}

		/// <summary>
		/// Describes specified statement.
		/// </summary>
		protected void It(string statementText, Action body)
		{
			EnsureThatStatementDeclaredInContext(statementText);

			SetStage(SpecificationDescriptionStage.Statement);
			var statement = new Statement(statementText, _beforeAction, body);
			_statements.Add(statement);
			RedoStage();
		}

		/// <summary>
		/// Indicates that this statement should be passed.
		/// </summary>
		protected void Pass()
		{
			Pass(null);
		}


		/// <summary>
		/// Indicates that this statement should be passed.
		/// </summary>
		protected void Pass(string reason)
		{
			if (reason == null)
			{
				throw new PassStatementException();
			}
			throw new PassStatementException(reason);
		}

		private void EnsureThatScopeIsNotNestedScope(string subject, string context)
		{
			if (!IsOutOfScope())
			{
				var scopeName = string.IsNullOrWhiteSpace(context)
					? subject
					: subject + " " + context;

				throw new SpecificationException(
					string.Format(
						"Statements scope \"{0}\" can not be declared as nested scope.",
						scopeName));
			}
		}

		private bool IsOutOfScope()
		{
			return CurrentStage == SpecificationDescriptionStage.OutOfScope;
		}

		private void EnsureThatStatementDeclaredInContext(string statement)
		{
			if (IsOutOfContext())
			{
				throw new SpecificationException(
					string.Format(
						"Statement \"{0}\" can not be declared out of context.",
						statement));
			}
		}

		private void EnsureThatBeforeActionDeclaredInContext()
		{
			if (IsOutOfContext())
			{
				throw new SpecificationException("Before action can not be declared out of context.");
			}
		}

		private void EnsureThatBeforeActionDeclaredOnce()
		{
			if (_beforeAction != null)
			{
				throw new SpecificationException("Before action can not be declared more then once.");
			}
		}

		private bool IsOutOfContext()
		{
			return CurrentStage != SpecificationDescriptionStage.Scope;
		}

		private SpecificationDescriptionStage CurrentStage
		{
			get { return _stages.Count == 0 ? SpecificationDescriptionStage.OutOfScope : _stages.Peek(); }
		}

		private void SetStage(SpecificationDescriptionStage stage)
		{
			_stages.Push(stage);
		}

		private void RedoStage()
		{
			if (_stages.Count != 0)
			{
				_stages.Pop();
			}
		}
	}
}